
/*
 *  Copyright 2007 Marc Lanctot and Marc Gendron-Bellemare
 *
 *  This file is part of OpenMTG.
 *
 *  OpenMTG is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  OpenMTG is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with OpenMTG.  If not, see <http://www.gnu.org/licenses/>.
 *
 */


import java.net.*;
import java.io.*;
import java.util.*;

public class Server {

  private ArrayList<ClientHandler> clientHandlers;
  private int port;
  private int clients;
  private int players;
  private ServerSocket serverSocket;

  private GameState state;
  private int firstTurnRequests; 
  private int turn; 
  private int turnPlayerId; 
  private HashMap<Integer, Boolean> endStackRequests;

  // turn-specific variables
  protected boolean playedLand;
  protected boolean attacked;
	
	protected HashSet<Integer> aWaitList;

  public Server(int port, int players)
  {
    this.port = port;
    this.players = players;
    clients = 0;
    clientHandlers = new ArrayList<ClientHandler>();
    state = new GameState();
		aWaitList = new HashSet<Integer>();
    turn = 0;
    firstTurnRequests = 0;
    endStackRequests = new HashMap<Integer, Boolean>();
	}

  public static void usage() 
  {
    System.out.println("java Server <options...>");
    System.out.println("");
    System.out.println("Valid options:");
    System.out.println("   --port <num>             (required)");
    System.out.println("   --players <num>          (opt. default = 2)");
    System.out.println("");
    System.exit(-1);
  }

  public static void main(String[] args) throws IOException {

    int port = -1, players = 2; 

    for (int i = 0; i < args.length; i++)
    {
      if (args[i].equals("--port")) 
        port = Integer.parseInt(args[++i]);
      else if (args[i].equals("--players")) 
        players = Integer.parseInt(args[++i]);
    }

    if (port < 0 || players < 0) 
      usage();

    Server s = new Server(port, players);
    s.run();

  }

  private void print(String str)
  {
    System.out.println("## SRV: " + str);
  }

  private void cleanup()
  {
    try {
      serverSocket.close(); 
    }
    catch(Exception e) { throw new RuntimeException(e); }
  }


  public void run()
  {

    print("Starting server, port = " + port);

    serverSocket = null;
    try {
      serverSocket = new ServerSocket( port );
    } catch (IOException e) {
      System.err.println("Could not listen on port: "+port);
      System.exit(1);
    }

    while (true)
    {
      print("Waiting for clients...");

      Socket clientSocket = null;
      try {
        clientSocket = serverSocket.accept();
      } catch (IOException e) {
        System.err.println("Accept failed.");
        System.exit(1);
      }
        
      print("Client accepted, " + clientSocket.toString() + ", adding to handlers");    

      PrintStream out; 
      BufferedReader in; 

      try {
        out = new PrintStream(clientSocket.getOutputStream());
        in = new BufferedReader(
             new InputStreamReader(
             clientSocket.getInputStream()));
      } 
      catch(Exception e) { throw new RuntimeException(e); }

      addClient(out, in, clientSocket);

    }

    //cleanup(); 

  }

  // communication 

  private void send(int clientNum, String s) {
    ClientHandler ch = clientHandlers.get(clientNum);
    ch.send(s);
  }

  public void broadcast(String s) { 
    for (ClientHandler ch : clientHandlers)
      ch.send(s);
  }

  // helpers
  
  public synchronized void addClient(PrintStream out, BufferedReader in, Socket clientSocket)
  {
    print("Adding client " + clients);
    
    ClientHandler ch = new ClientHandler(clients, out, in, this, clientSocket);
    clients++;
    clientHandlers.add(ch); 
    ch.start();
    
  }
  
  public synchronized void removeClient(ClientHandler ch)
  {
    for (int i = 0; i < clientHandlers.size(); i++) {
      ClientHandler ch_p = clientHandlers.get(i); 
      
      if (ch_p == ch) {
        
        print("Removing client " + i);
        
        ch.kill();
        clientHandlers.remove(i);
        clients--;
        break;
      }
    }
  }
  
  // State functions. Called by client handlers.
  
  public synchronized int getPlayers() {
    return players;
  }
  
  public synchronized int addPlayer(Player p) 
  {
    if (state.getNumPlayers() >= players) 
      return 1;
    
    if (state.containsPlayerWithName(p.getName()))
      return 2;

    state.addPlayer(p);
    return 0;
  }

  public synchronized boolean setDeck(int playerId, Deck d)
  {
    state.setDeck(playerId, d);
    return true;
  }

	public synchronized PhaseInfo.Phase getPhase()
	{
		return state.getPhase();
	}

	public synchronized boolean isMainPhase()
	{
		// @@@ return false if in combat
		return (state.getPhase().equals(PhaseInfo.Phase.PREMAIN) ||
			state.getPhase().equals(PhaseInfo.Phase.POSTMAIN));
	}

	public synchronized PhaseInfo getPhaseInfo()
	{
		return state.getPhaseInfo();
	}
	
	public synchronized boolean isEndTurn()
	{
		return state.getPhase().equals(PhaseInfo.Phase.END);
	}

	public synchronized boolean isGameOn()
	{
		return (turn != 0);
	}
	
  public synchronized void checkStart()
  {
    if (state.getNumPlayers() == players && state.ready())
      startGame();
  }

  public int whoDecides() { 
    return state.whoDecides(); 
  }

  public void setWhoStarts(int whoStarts)
  {
    int starter = state.setWhoStarts(whoStarts);
    broadcast("PLAYERSTART " + starter);
    turnPlayerId = starter;
  }

  public synchronized void startGame() 
  {
    // player numbers are iids for the players
    // rest of the first 100 are reserved
    //Helpers.next_iid = 100;

    for (int i = 0; i < state.getNumPlayers(); i++)
    {
      Player p = state.getPlayer(i); 

      Deck d = p.getDeck(); 
      d.init(p.getId()); 
      Dealer.linearShuffle(d);     
      
      p.draw(7);
      
      broadcast("PLAYERINFO " + p.getInfo()); 
    }

    // send hand info for each player
    for (int i = 0; i < state.getNumPlayers(); i++)
      sendHand(i);

    broadcast("GAMESTART " + state.whoDecides()); 
  }

	/** Takes the next action in the game. This functions is called at regular 
	 *  intervals by all client handlers. 
	 *  
	 * In general this may either go to the next phase, or request the 'ok'
	 * from one of the players.
	 * 
	 * IMPORTANT: This method may be called by any of the ClientHandlers,
	 * not necessarily the current player's. Do not rely on the same handler
	 * repeatedly calling this function!
	 * 
	 * @return true if the server should pass the control to the player,
	 *   false if it should go to the next phase.
	 */
		
	public synchronized boolean actPhase()
	{
		PhaseInfo phaseInfo = state.getPhaseInfo();
		Player curPlayer = state.getPlayer(turnPlayerId);
		
		boolean passControl = false;

		if (phaseInfo.getNewPhase())
		{
			passControl |= enterNewPhase();
			// @@@ bad?
			phaseInfo.setNewPhase(false);
		}

		PhaseInfo.Phase current = state.getPhase();

		switch (current)
		{
			case NEWTURN: // Simply move to the untap phase
				nextPhase();
				break;
			case UNTAP:
				nextPhase();
				break;
			case UPKEEP:
				nextPhase();
				break;
			case DRAW:
				nextPhase();
				break;
			case PREMAIN:
				passControl = true;
				waitForGo(turnPlayerId);
				break;
			case COMBAT:
				if (state.canAttack(turnPlayerId))
				{
					passControl = true;
					waitForGo(turnPlayerId);
				}
				else
					nextPhase();
				break;
			case COMBATINSTANT:
			case BLOCKINSTANT:
			case DAMAGEINSTANT:
				// Skip the combat phases if not attacking
				if (!state.isAttacking())
					nextPhase();
				else
					passControl = true;
				break;
			case BLOCK:
				if (!state.isAttacking())
					nextPhase();
				else
					passControl = true;
				break;
			case FIRSTDAMAGE:
				nextPhase();
				break;
		  case ASSIGNDAMAGE:
        // Determine whether any more damage needs to be assigned
        if (!state.isAttacking())
          nextPhase();
        else if (state.doneDamageAssignment())
          nextPhase();
        else
          passControl = true;
        break;
			case DAMAGE:
				// Should wait if there is damage prevention to be done!
				nextPhase();
				break;
			case POSTMAIN:
				passControl = true;
				waitForGo(turnPlayerId);
				break;
			case DISCARD:
				if (curPlayer.mustDiscard())
					passControl = true;
				else
					nextPhase();
				break;
			default:
				throw new IllegalArgumentException ("No phase: "+current);
		}
	
		// Now check whether we changed phases; apply the events that occur on
		//  the beginning of a phase
		if (phaseInfo.getNewPhase())
		{
			passControl |= enterNewPhase();
			phaseInfo.setNewPhase(false);
		}

		return passControl;
	}

	public synchronized boolean enterNewPhase()
	{
		boolean passControl = false;
		
		PhaseInfo.Phase newPhase = state.getPhase();
		if (state.getNewPhase())
		{
			switch (newPhase)
			{
				case UNTAP:
					passControl = enterUntapPhase();
					break;
				case DRAW:
					passControl = enterDrawPhase();
					break;
				case DISCARD:
					passControl = enterDiscardPhase();
					break;
				case COMBAT:
					passControl = enterCombatPhase();
					break;
				case BLOCK:
					passControl = enterBlockPhase();
					break;
        case ASSIGNDAMAGE:
          passControl = enterAssignDamagePhase();
          break;
        case DAMAGE:
          passControl = enterDamagePhase();
          break;
				case COMBATINSTANT:
				case BLOCKINSTANT:
        case DAMAGEINSTANT:
					passControl = enterInstantPhase();
					break;
				case END:
					passControl = true;
					break;
			}
		}

		return passControl;
	}

  public synchronized boolean enterDrawPhase()
  {
		if (turn == 1) // No card for this sucker
			return false;

    Player currentPlayer = state.getPlayer(turnPlayerId);

    Card c = currentPlayer.draw();
    int libsize = currentPlayer.getDeck().size();

    for (int id = 0; id < state.getNumPlayers(); id++) {
      ClientHandler ch = clientHandlers.get(id);

      if (id == turnPlayerId) 
        ch.send("DRAW " + c.encode() + " " + libsize);
      else
        ch.send("PLAYERDRAW " + turnPlayerId + " 1 " + libsize); 
    }

		return false;
  }

	public synchronized boolean enterUntapPhase()
	{
    Player currentPlayer = state.getPlayer(turnPlayerId);
    ArrayList<Integer> untapIids = new ArrayList<Integer>();
	
		// All cards played last turn are now un-sick
		broadcast("UNSICK "+turnPlayerId);
	
		String untapString = "";
		
		// Go through all cards in play and untap
		// Separate code for each type since some cards affect untapping of
		//  a particular group (e.g. Smoke)
		for (Card c : currentPlayer.getLands())
		{
			System.err.println ("Untapping "+c.encode());
			c.untap();
			c.setSickness(false);
			untapString += c.getIID()+" ";
		}
		untapString += "! ";
		for (Card c : currentPlayer.getCreatures())
		{
			c.untap();
			c.setSickness(false);
			untapString += c.getIID()+" ";
		}
		untapString += "! ";

		for (Card c : currentPlayer.getArtifacts())
		{
			c.untap();
			c.setSickness(false);
			untapString += c.getIID()+" ";
		}
		untapString += "! ";
		for (Card c : currentPlayer.getEnchantments())
			c.setSickness(false);
	
		broadcast("UNTAP " + turnPlayerId + " " + untapString);
		
		return false;
	}
	
	public synchronized boolean enterDiscardPhase()
	{
		// Determine whether the player needs to discard anything
    Player currentPlayer = state.getPlayer(turnPlayerId);
    ClientHandler ch = clientHandlers.get(turnPlayerId);

		// @@@ no no - shouldn't be heard-coded
		int overCount = currentPlayer.getHandSize() - currentPlayer.getMaxHandSize();
		if (overCount > 0)
		{
			ch.send("QUERYDISCARD "+overCount);
			currentPlayer.addToDiscard(overCount);
			// Also prevent the player from leaving this phase via 'go'
			setPreventGo(true);
			return true;
		}
		
		else
			return false;
	}
	
	public synchronized boolean enterCombatPhase() {
		// @@@ at this point, no attack phase has been created ->
    //  state.isAttacking() is necessarily false
    if (state.isAttacking())
		{
      System.out.println ("Waiting on the attacking player...");
			waitForGo (turnPlayerId);
			return true;
		}
		else
			return false;
	}
	
	public synchronized boolean enterBlockPhase() {
		// Here we must wait for the OTHER player to 'go'
		if (state.isAttacking())
		{
			Player defender = state.getDefendingPlayer();
			waitForGo(defender.getId());
			return true;
		}
		else
			return false;
	}
	
	public synchronized boolean enterInstantPhase() {
		if (state.isAttacking())
		{
			// Ask ALL players to 'go'
			for (int p = 0; p < state.getNumPlayers(); p++)
				waitForGo(p);
			return true;
		}
		else
			return false;
	}
	
  public synchronized boolean enterAssignDamagePhase()
  {
    boolean needsControl = false;

    if (state.isAttacking())
	  {
      // If any of the damage has to be assigned manually, wait for this 
      //  to be done
      needsControl = resolveAttack();
      waitForGo(-1);
    }

    return needsControl;
  }
  
  public synchronized boolean enterDamagePhase()
  {
    if (state.isAttacking())
    {
      resolveDamage();
    }
    return false; 
  }
	
	public synchronized boolean resolveAttack()
	{
		ArrayList<Message> responses = new ArrayList<Message>();

		List<CombatGroup> manual = state.resolveAttack(responses);
		sendMessages(responses);

    return !manual.isEmpty();
	}

  public synchronized void resolveDamage()
	{
		ArrayList<Message> responses = new ArrayList<Message>();

		state.cleanupAttack();
		state.cleanupDead(responses);

		// Broadcast:
		// @@@ dead critters
		// @@@ life points damage
		// @@@ other effects
		sendMessages(responses);
	}
	
  public synchronized void sendHand(int id) {
    ClientHandler ch = clientHandlers.get(id);

    Player p = state.getPlayer(id);
    int libsize = p.getDeck().size(); 

    for (Card c : p.getHand()) 
      ch.send("DRAW " + c.encode() + " " + libsize);
  }

  public boolean mulligan(int id)
  {
    boolean succ = state.mulligan(id);

    return succ;
  }

  public synchronized int getTurn() { return turn; }

	public synchronized int getTurnPlayerId() { return turnPlayerId; }

  public synchronized void firstTurnRequest()
  {
    firstTurnRequests++;

    if (firstTurnRequests == state.getNumPlayers())
    {
      broadcast("MSG Game on!"); 

      turn = 1; 

      startTurn();
    }
  }

  public synchronized void startTurn()
  {
		state.newTurn();

    endStackRequests.clear();
    broadcast("STARTTURN " + turnPlayerId + " " + turn);
  }

  public synchronized Card getCard(int iid) {
    return state.getCard(iid);
  }

  public synchronized Card playLand(int pid, int iid)
  {
    // @@@ uncomment me!
    /* if (state.getPlayedLand())  // already played a land this turn!
      return null; */

    Card card = state.playLand(pid, iid);

    if (card != null)
			state.setPlayedLand(true);

    return card;
  }

  public synchronized boolean tapForMana(int pid, int iid)
  {
    return state.tapForMana(pid, iid);
  }

  public synchronized Card cast(int pid, int iid)
  {
    return state.cast(pid, iid, turnPlayerId);
  }

  public boolean attack(int a_pid, int d_pid, int iid) 
  {
		System.err.println (a_pid+" -> "+d_pid+" : "+iid + " "+state.canAttack(a_pid));
    if (!state.canAttack(a_pid))
      return false;

		// GameState takes care of tapped, sickness, etc
    return state.attack(a_pid, d_pid, iid); 
  }

	public boolean block(int pPID, int pBlocker, int pAttacker)
	{
    ClientHandler ch = clientHandlers.get(pPID);
		if (!state.canBlock(pPID))
		{
			ch.send ("ERROR in blocking "+pBlocker+" "+pAttacker);
			return false;
		}

		return state.block(pPID, pBlocker, pAttacker);
	}

	/** Notifies the game that this player is ready to proceed to the next 
	  * phase. */
	public synchronized boolean go(int pClientNum)
	{
    ClientHandler ch = clientHandlers.get(pClientNum);
		Integer intNum = new Integer(pClientNum);
		
		// Check whether this player is being waited upon; don't allow players
		//  to go while resolving spell effects
		if (!aWaitList.contains(intNum) || !state.stackIsEmpty())
		{
			ch.send("ERROR Hold on to your horses...");
			return false;
		}
		else
		{
			// Remove him from the wait list
			aWaitList.remove(intNum);
			if (aWaitList.isEmpty())
				nextPhase();
			
			return true;
		}
	}

	/** If this function is called with pNoGo = true, the 'go' command will
	  * not change phases. This is used for things such as discard or upkeep
		* where the player must do something to complete his/her phase.
		*/
	public synchronized void setPreventGo(boolean pNoGo)
	{
		// Tricked! We simply add a player which doesn't exist (and so can't go)
		//  to the wait list.
		if (pNoGo)
			aWaitList.add (new Integer(-1));
		else
			aWaitList.remove (new Integer(-1));
	}

	public synchronized void waitForGo (int pPID)
	{
		aWaitList.add (new Integer(pPID));
	}
	
	public synchronized void nextPhase()
	{
			state.nextPhase();
			broadcast("PHASE "+state.getPhase());
			aWaitList.clear();
	}

	/** This function should be called once the end-of-turn phase is reached
	  * (and NOT to reach the end-of-turn phase). It passes the control to
		* the next player via startTurn().
		*/
  public synchronized void endTurn() 
  {
    turnPlayerId = (turnPlayerId + 1) % players;
    turn++;

	  startTurn();	
  }

	public synchronized boolean sendMessages (ArrayList<Message> pMessages)
	{
    for (Message m : pMessages)
    {
      if (m.id == -1)
        broadcast(m.data);
			else
			{
    		ClientHandler ch = clientHandlers.get(m.id);
				ch.send(m.data);
			}
    } 

		return true;
	}

	public synchronized void discard(int pPID, ArrayList<Integer> pCards)
	{
		Player sucker = state.getPlayer(pPID);
    ClientHandler ch = clientHandlers.get(pPID);
	
		// Check if the sneaky bastard is trying to discard too many cards
		if (pCards.size() > sucker.getNumCardsDiscard())
		{
			ch.send("ERROR Too many cards to be discarded.");
			return;
		}
	
		ArrayList<Message> messages = new ArrayList<Message>();

		// Make sure this person is allowed to discard!
		String toDiscard = "";
		for (Integer cIdx : pCards)
		{
			// Check if the card exists and belongs to the player
			Card c = sucker.getCardInHand(cIdx.intValue());
			if (c == null || c.getOID() != pPID)
				ch.send("ERROR Discard "+pPID+" "+cIdx.intValue());
			else
			{
				toDiscard += " " + c.getCID()+","+c.getIID();
				sucker.discard(c,messages);
			}
		}

		// If no more cards, stop discarding
		if (sucker.getHandSize() == 0)
			while (sucker.mustDiscard()) sucker.fakeDiscard();

		broadcast("DISCARD "+pPID+toDiscard);
		sendMessages(messages);

		// Once we've discarded all cards, allow the player to 'go'
		if (!sucker.mustDiscard())
			setPreventGo(false);
	}
	
	public boolean assignDamage(int pid, int victim, int from, int damage)
	{
	  return state.assignDamage(pid, victim, from, damage);
	}

  public boolean endStackRequest(int playerNum)
  {
    endStackRequests.put(playerNum, true); 
    
    if (endStackRequests.size() == players)
    {
      endEffectStack();
      endStackRequests.clear(); 
      return true;
    }
    
    return false;
  }
  
  public void endEffectStack() 
  {
    // broadcasts
    ArrayList<Message> responses = new ArrayList<Message>();
    
    state.resolveStack(responses);
    
    // deal with responses
  	sendMessages(responses);
  }
  
  public boolean d_available() 
  {
    if (state.getStackSize() > 0)
      return true; 
    
    // might be more stuff here ..? or eventually?
    
    return false;
  }
  
}
